Lab 09 - alokacja pamieci, polimorfizm, tekstury
Lab 09 - Alokacja pamięci, polimorfizm, tekstury w SFML
C++17 standard
Wykorzystywane na tych i następnych zajęciach elementy języka C++ zostały wprowadzone do standardu jako C++17. Domyślnie QT Creator generuje projekt zgodny ze stndardem C++11, co oznacza, że niezbędna jest zmiana ustawień projektu. Otwórz plik *.pro i zmień linijkę:
CONFIG += console c++11
na
CONFIG += console c++17
Pamiętaj o wywołaniu polecenie Run qmake na pliku *.pro.
Wskaźniki
Zmienne, które tworzyliśmy do tej pory w programach poprzez prostą deklarację zmiennej miały swój czas życia ograniczony do jednego zakresu (scope) - fragmentu kodu, najczęściej wydzielonego klamrami, np. ciała funkcji, czy wnętrza pętli. Zmienne te były tworzone w sekcji pamięci operacyjnej zwanej stosem. Zmienne i obiekty takie są automatycznie usuwane w momencie wyjścia poza scope, w którym zostały zaalokowane, np. opuszczenia funkcji. W większości przypadków jest to działanie pożądane i zwalnia programistę z konieczności ręcznego zarządzania pamięcią.
Istnieją jednak przypadki, w których wskazane jest oddzielenie czasu życia obiektu od zakresu w którym został on utworzony. W tym celu używamy alokacji na stercie.
W celu alokacji zmiennych na stercie klasycznie używano operatora
new
. Tak utworzoną zmienną należało następnie usunąć za
pomocą operatora delete
. Jako że o operacji dealokacji
łatwo zapomnieć, wiele programów do dziś przejawia problem nazywany
wyciekiem pamięci.
🛠🔥 Zadanie 🛠🔥
Wykonaj poniższy kod obserwując użycie pamięci w Menedżerze zadań (Ctrl + Alt + Delete):
const int number_of_elements = 100000000;
for (int i = 0; i < number_of_elements; i++) {
int *number = new int;
}
std::cout << "Utworzono " << number_of_elements << " wskaznikow na wartosc typu int. Nacisnij jakis klawisz aby kontynuowac." << std::endl;
std::getchar();
W celu unikania wycieków pamięci, w C++11 wprowadzono nowe typy tzw.
inteligentnych wskaźników (ang. smart pointer), w tym
std::unique_ptr
oraz std::shared_ptr
. W tej
instrukcji pominiemy std::shared_ptr
, ponieważ służy on do
przechowywania obiektów, które mają wielu właścicieli (na przykład
znajdują się jednocześnie w więcej niż jednym wektorze).
std::unique_ptr
nie może być kopiowany - obiekt na który
wskazuje ma tylko jednego właściciela. W celu wykorzystania
inteligentnych wskaźników zaimportować
#include <memory>
.
std::unique_ptr
to klasa, która w uproszczeniu,
przejmuje odpowiedzialność nad czasem życia wskaźnika. W
konstruktorze wykonuje alokację obiektu przy pomocy new
, a
w destruktorze niszczy go za pomocą delete
. Zastępując
wykorzystywanie zwykłych wskaźników w stylu języka C użyciem
std::unique_ptr
można łatwo zapobiec wyciekom pamięci.
Zmienna typu std::unique_ptr
automatycznie wywoła
delete
na przechowywanym wskaźniku kiedy wyjdzie poza
zakresu, w którym została zadeklarowana.
🛠🔥 Zadanie 🛠🔥
Zmień poprzedni kod tak, aby zamiast operatora new
wykorzystywał typ std::unique_ptr
. Do stworzenia
std::unique_ptr
wykorzystaj funkcję std::make_unique
.
Obserwuj użycie pamięci.
Wskazówka: jako, że do funkcji
std::make_unique
przekazujemy typ zmiennej, którą chcemy
utworzyć na stercie możemy opcjonalnie użyć słowa
kluczowego auto
w celu ominięcia deklarowania obiektu o
długiej nazwie typu: std::unique_ptr<int>
. Nie
spowoduje to utraty czytelności kodu.
Po przeczytaniu powyższego opisu i wykonaniu zadania można odnieść
wrażenie, że alokacja na stosie oraz alokacja na stercie z użyciem
std::unique_ptr
są funkcjonalnie identyczne. W obu
przypadkach obiekt niszczony jest po wyjściu z zakresu w którym został
zadeklarowany. Po co więc używać std::unique_ptr
?
W C++11, obok typu std::unique_ptr
wprowadzono również
move semantics - możliwość “przeniesienia” własności obiektu do
innego zakresu (scope) lub kontenera. O ile w przypadku
obiektów zadeklarowanych na stosie owo przeniesienie w wielu przypadkach
spowoduje konieczność wykonania kopii przynajmniej części pól
przenoszonego obiektu, to w przypadku std::unique_ptr
przeniesienie nie spowoduje kopiowania obiektu na stercie.
🛠🔥 Zadanie 🛠🔥
Spróbuj skompilować poniższy kod:
std::vector<std::unique_ptr<int>> some_pointers;
const int number_of_elements = 100000000;
for (int i = 0; i < number_of_elements; i++) {
auto number = std::make_unique<int>();
.emplace_back(number);
some_pointers}
std::cout << "Umieszczono " << number_of_elements << " elementow w std::vector<std::unique_ptr<int>>. Nacisnij jakis klawisz aby kontynuowac." << std::endl;
std::getchar();
Zastanów się dlaczego tak napisany program nie działa.
Pamiętając, że obiekt na który wskazuje std::unique_ptr
może mieć tylko jednego właściciela, przekaż “prawo własności” wektorowi
wywołując na zmiennej number
funkcję std::move
.
Polimorfizm
Użycie wskaźników na obiekty ze sterty zamiast obiektów na stosie pozwala na łatwe zinterpretowanie wskaźnika na obiekt klasy pochodnej jako wskaźnika na obiekt klasy bazowej.
🛠🔥 Zadanie 🛠🔥
Wykorzystując przykładowe klasy z poprzedniej instrukcji spróbuj
skompilować poniższy kod. Pokazuje on w jaki sposób możliwe jest
przechowywanie elementu klasy pochodnej (Car
) we wskaźniku
na klasę bazową (Vehicle
).
std::unique_ptr<Vehicle> skoda_superb_as_vehicle = std::make_unique<Car>(
"Skoda Superb", "Gasoline", 200, true);
std::cout << "Name: " << skoda_superb_as_vehicle->name() << std::endl;
std::cout << "Has ABS: " << skoda_superb_as_vehicle->has_abs() << std::endl;
Wiedząc, że traktujemy nasz samochód jako obiekt klasy
Vehicle
oraz analizując metody które są w tej klasie
zaimplementowane zastanów się dlaczego tak napisany program nie działa.
Usuń linijkę powodującą błąd kompilacji.
Zwróć uwagę, na różnicę w odwoływaniu się do pól obiektu
(->
zamiast .
). Z czego ona wynika?
Aby klasa bazowa była polimorficzna musi posiadać przynajmniej jedną
metodę wirtualną (przydomek virtual
). Metody wirtualne
pozwalają na ich przeciążanie w klasie pochodnej w taki sposób, że gdy
wywołamy je poprzez wskaźnik do klasy bazowej zostanie wywołana właściwa
implementacja z klasy pochodnej. Gdyby funkcja nie miała przydomka
virtual
to jej wywołanie poprzez wskaźnik do klasy bazowej
wywołałoby implementację z klasy bazowej, nie zważając na to, że w
klasie pochodnej znajduje się przeciążona wersja funkcji.
Niezwykle istotne jest aby każda bazowa klasa polimorficzna zawierała deklarację wirtualnego destruktora. Jeśli taka deklaracja nie znajdzie się w klasie bazowej to usuwanie obiektu klasy pochodnej poprzez wskaźnik do klasy bazowej jest niezdefiniowanym zachowaniem i może prowadzić do wycieku pamięci, gdyż wywołany zostanie tylko destruktor klasy bazowej. Najprostsza deklaracja wirtualnego destruktora wygląda następująco:
virtual ~ClassName() = default;
🛠🔥 Zadanie 🛠🔥
Do klasy Vehicle
dodaj wirtualny destruktor (jako
public
).
Rozwiązana została kwestia poprawnego wywoływania destruktora.
Jednak nadal nie jest możliwe wywołanie metody has_abs dla
obiektu skoda_superb_as_vehicle , a przecież obiekt ten
został utworzony z wykorzystaniem konstuktora klasy
Car . |
W celu ponownego zinterpretowania wskaźnika do klasy bazowej jako
wskaźnika lub referencji do klasy pochodnej należy użyć wyłuskania
wskaźnika (“wyciągnięcia” obiektu na który wskaźnik wskazuje) oraz
specjalnej funkcji dynamic_cast . Poniższy przykład pozwala
wywołać metodę has_abs . |
cpp Car *skoda_superb_as_as_car = dynamic_cast<Car *>(skoda_superb_as_vehicle.get()); std::cout << "Has ABS: " << skoda_superb_as_as_car->has_abs() << std::endl; |
#### 🛠🔥 Zadanie 🛠🔥 |
Dodaj definicję klasy Bike z poprzednich zajęć,
przeanalizuj oraz wykonaj następujący kod: |
```cpp std::vector<std::unique_ptr |
for (int i = 0; i < vehicles.size(); i++) { Car &some_car = dynamic_cast<Car &>(*vehicles[i]); } ``` |
Dlaczego zwracany jest wyjątek? |
Możemy rozwiązać ten problem przechwytując wyjątek klauzulą
try...catch lub rzutować wskaźnik i sprawdzać czy wartość
po rzutowaniu jest niezerowa: |
cpp for (int i = 0; i < vehicles.size(); i++) { Car *some_car = dynamic_cast<Car *>(vehicles[i].get()); if (some_car != nullptr) { // cast successful std::cout << i << ": abs=" << some_car->has_abs() << std::endl; } else { // nope std::cout << i << ": not a Car" << std::endl; } } |
Uwaga: zauważ, że w tym przypadku, z uwagi na
bezpośrednie przekazanie wyniku działania funkcji
std::make_unique do metody emplace_back (bez
użycia zmiennej do której przypisujemy wartość zwracaną przez
std::make_unique ), nie było konieczności używania
std::move . |
Polimorfizm a SFML
Wszystkie “rysowalne” klasy z biblioteki SFML dziedziczą po klasie
polimorficznej sf::Drawable
. Oznacza to, że możemy
przechowywać wszystkie obiekty sceny w jednym kontenerze jako
std::unique_ptr<sf::Drawable>
. Oznacza to, że np. w
naszej grze, nie ma konieczności posiadania osobnych kontenerów dla
drzew, krzewów, kwiatów czy innych obiektów. Wszystkie podobne do siebie
elementy zgrupujemy możemy zgrupwać razem co upraszcza kod i zmniejsza
liczbę powtórzeń.
🛠🔥 Zadanie 🛠🔥
Korzystając z teorii zamieszczonej powyżej utwórz w projekcie SFML wspólny wektor kilku obiektów o różnych kształtach i kolorach.
W pętli głównej programu wywołuj metodę draw na wyłuskanych ze
wskaźnika obiektach sf::Drawable
.
Wykorzystaj kod z instrukcji [Lab 07 - Wprowadzenie do SFML] i umieść utworzone koło, prostokąt i trójkąt w wektorze:
std::vector<std::unique_ptr<sf::Drawable>> shapes;
Zamiast osobno wywoływać rysowanie dla poszczególnych figur wykorzystaj:
for(const auto &s : shapes) {
.draw(*s);
window}
Następnie utwórz funkcję, w której zamieścisz cały kod do generacji figur. Funkcja powinna zwracać utworzony kontener.
void create_shapes(std::vector<std::unique_ptr<sf::Drawable>> &shapes)
Tekstury/sprite’y w SFML
Teksturowanie to nanoszenie na kształty geometryczne płaskiego, dwuwymiarowego obrazu, tak aby nadać odzworowywanemu przedmiotowi wygląd bliższy prawdziwemu.
W przypadku grafiki dwuwymiarowej używa się pojęcia sprite’u - płaskiej bitmapy, która w całości lub fragmencie renderowana jest w określonym miejscu na ekranie.
Skąd wziąć tekstury?
Istnieje wiele witryn internetowych z darmowymi zasobami multimedialnymi, takimi jak tekstury, sprite’y czy dźwięki - w szczególności do zastosowań niekomercyjnych. Bazy mają zazwyczaj dobrze zorganizowany katalog, co ułatwia znalezienie odpowiedniego zasobu.
Pamiętaj - jeśli korzystasz z grafiki pobranej z Internetu, sprawdź na jakiej licencji zostały udostępnione zasoby. Często autorzy oczekują jedynie tzw. uznania autorstwa (ang. attribution) - wzmianki w informacjach o programie, skąd pochodzą zasoby i kto jest ich autorem.
Poniżej umieszczono odnośniki do kilku tekstur, od których możesz rozpocząć: grass, wall, guy.
Użycie tekstur
W SFML teksturą nazywamy obraz, który przechowywany jest w pamięci, natomiast sprite to kształt, który będzie wyświetlony na ekranie, i który może być powiązany z daną teksturą.
Dodatkowy opis wykorzystania sprite’ów można znaleźć tutaj
Najprostsze użycie tekstury polega na wczytaniu bitmapy z pliku do
obiektu sf::Texture
, a następnie utworzeniu obiektu
sf::Sprite
i powiązaniu z załadowaną teksturą.
::Texture texture;
sfif (!texture.loadFromFile("grass.png")) {
std::cerr << "Could not load texture" << std::endl;
return 1;
}
::Sprite sprite;
sf.setTexture(texture); sprite
Klasa sf::Sprite
dziedziczy z klas
sf::Drawable
oraz sf::Transformable
, zatem ma
dostępne metody, które pozwalają na transformacje geometryczne
(przesuwanie, obracanie, skalowanie). Ponadto, dzięki polimorfizmowi
sprite’y mogą być przechowywane w jednym kontenerze wraz z kształtami
takimi jak sf::RectangleShape
.
🛠🔥 Zadanie 🛠🔥
Wczytaj do programu dostarczone tekstury.
Utwórz po jednym sprite dla każdej z nich, wyświetl je na ekranie w
różnych miejscach i w różnej skali
(sf::Transformable::setScale
).
Jeśli chcemy, aby dany sprite wyświetlał jedynie fragment powiązanej
z nim tekstury, możemy użyć na nim metody
setTextureRect(sf::Rect)
, która pozwoli nam wskazać wybrany
obszar. Wykorzystaj tę metodę do wyświetlenia tylko twarzy wczytanej
postaci.
::Texture texture_guy;
sfif(!texture_guy.loadFromFile("guy.png")) { return 1; }
::Sprite guy;
sf.setTexture(texture_guy);
guy.setTextureRect(sf::IntRect(10, 20, 20, 15)); //left, top, width, height guy

Jeśli wybierzemy do wyświetlenia obszar większy niż rozmiar tekstury,
a dodatkowo przestawimy teksturę w tryb Repeated (metoda
sf::Texture::setRepeated(bool)
, uzyskamy
kafelkowanie (ang. tiling), czyli tekstura będzie
powielana sąsiadująco tak aby wypełnić cały obszar. Przeanalizuj:
::Texture texture_wall;
sfif(!texture_wall.loadFromFile("wall.png")) { return 1; }
.setRepeated(true);
texture_wall
::Sprite wall;
sf.setTexture(texture_wall);
wall.setScale(0.3, 0.3);
wall.setTextureRect(sf::IntRect(0, 0, 500, 500)); wall

🛠🔥 Zadanie 🛠🔥
Wykorzystaj kafelkowanie i teksturę grass, aby utworzyć tło
w programie. Ustal odpowiedni TextureRect
dla sprite’a
reprezentującego tło tak, aby obejmował cały rozmiar okna.
Ważne: dowiązanie tekstury do sprite’a nie powoduje
skopiowania jej zawartości - jeśli obiekt sf::Texture
zostanie usunięty i spróbujemy wyświetlić sprite, z którym był
powiązany, w miejsce tekstury pojawi się białe wypełnienie.
Warto też ograniczyć liczbę tekstur i operacji na nich do minimum - na przykład używać tej samej tekstury dla wielu obiektów. W przypadku prostych gier, które nie mają wiele zasobów, najlepiej wczytać wszystkie niezbędne tekstury na początku uruchamiania programu i utrzymywać w pamięci aż do jego zakończenia.
Animacje
Animacje w grach zbudowanych na sprite’ach opierają się na zestawie następujących po sobie klatek animacji, przełączanych w odpowiednich momentach.
Często kolejne klatki animacji umieszczone są w jednej teksturze, a
wybór danej klatki odbywa się poprzez wskazanie odpowiedniego fragmentu
tekstury (przypomnienie: metoda setTextureRect(sf::Rect)
.
Dzięki temu cała animacja może być przechowywana w pamięci układu
graficznego, a przejście do kolejnej klatki animacji nie jest kosztowne
obliczeniowo.
🛠🔥 Zadanie 🛠🔥
Napisz klasę AnimatedSprite
dziedziczącą po
sf::Sprite
.
Klasa powinna mieć interfejs, który pozwoli na wskazanie fragmentów tekstury będących kolejnymi klatkami animacji:
int animation_fps = 7;
(animation_fps);
AnimatedSprite hero
/* add texture, set parameters, etc. */
.add_animation_frame(sf::IntRect(200, 0, 37, 37)); // 1 frame of animation
hero.add_animation_frame(sf::IntRect(250, 0, 37, 37)); // 2 frame
hero.add_animation_frame(sf::IntRect(300, 0, 37, 37)); // 3 frame
hero.add_animation_frame(sf::IntRect(350, 0, 37, 37)); // 4 frame hero
Klasa powinna mieć metodę step
, dzięki której będzie
możliwe informowanie obiektu o czasie jaki upłynął. Klasa na podstawie
tego czasu oraz wewnętrznego parametru opisującego liczbę klatek na
sekundę, powinna decydować, czy ma nastąpić przełączenie na kolejną
klatkę animacji.
Stwórz na scenie obiekt klasy AnimatedSprite
,
wykorzystaj teksturę postaci z załączonego zestawu tekstur.
Grawitacja
Grawitacja to stałe przyspieszenie działające w pionowym kierunku. Tak samo, jak w każdym kwancie czasu bieżąca prędkość wpływa na położenie obiektu, tak bieżące przyspieszenie wpływa na prędkość.
🛠🔥 Zadanie 🛠🔥
Dodaj do klasy AnimatedSprite
informację o prędkości
obiektu, analogicznie do klasy CustomRectangleShape
. Dodaj
kolejną pochodną położenia - przyspieszenie i dodaj odpowiednie
oblicznenia w metodzie step
. Ustaw pionową składową
przyspieszenia tak, aby obiekt “spadał”. Dodaj odpowiednie parametry i
warunki opisujące prędkość graniczną.
Zadania końcowe 🛠🔥
Labirynt
Wykorzystując dostarczone tekstury zbuduj grę Labirynt
- ustaw tło gry
- dodaj do sceny kilka obiektów - ścian z odpowiednią teksturą
- dodaj postać, którą będziesz mógł poruszać klawiszami kursora
- dodaj detekcję kolizji tak, aby niemożliwe było przechodzenie przez ściany
Ostateczny efekt powinien być podobny do tego na filmie.
Platformer
Bazując na kodzie służącym do wykrywania kolizji w zadaniu z poprzednich zajęć, napisz prosty silnik gry platformowej:
- dodaj do sceny kilka obiektów - platform z odpowiednią teksturą
- dodaj postać -
AnimatedSprite
, którą będziesz mógł poruszać klawiszami kursora w lewo i w prawo, ustal dla niej grawitację - dodaj kod wykrywający kolizje pomiędzy postacią a którąkolwiek z platform, tak aby postać mogła stać na platformach
- dodaj skakanie - jeśli postać dotyka podłoża, nadaj jej prędkość początkową w górę
Autorzy: Michał Fularz, Tomasz Mańkowski, Dominik Pieczyński, Jakub Tomczyński